﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Reflection;
using System.Diagnostics;

namespace MCB.Reflection.Extensions
{
    /// <summary>
    /// The type of write being performed
    /// </summary>
    public enum WriteTypeEnum { WriteStart, WriteEnd }

    /// <summary>
    /// Extensions for object printing
    /// </summary>
    public static class TypePrintExtensions
    {
        /// <summary>
        /// Prints an objects structure and contents
        /// </summary>
        public static string Print(this object value, string prefix)
        {
            return Print(value, null, prefix, null, null, DefaultPrintObjectValue);
        }

        /// <summary>
        /// Prints an objects structure and contents
        /// </summary>
        public static string Print(this object value)
        {
            return Print(value, null, value.GetType().Name, null, null, DefaultPrintObjectValue);
        }

        /// <summary>
        /// Prints an objects structure and contents
        /// </summary>
        public static string Print(
            this object value,
            PrintFormatDelegate format)
        {
            return Print(value, null, value.GetType().Name, null, null, format);
        }

        /// <summary>
        /// Delegate for formatting objects
        /// </summary>
        public delegate string PrintFormatDelegate(PrintFormatArguments arguments);

        /// <summary>
        /// Prints an objects structure and contents
        /// </summary>
        private static string Print(
            this object value,
            Type type,
            string path, 
            MemberInfo member,
            object index,
            PrintFormatDelegate format)
        {
            //Locals
            Type workingType = (type==null)?value.GetType():type;
            StringBuilder builder = new StringBuilder();
            string childPrefix;
            int workingIndex = 0;
            bool writeEnd = false;
            Type memberType = null;
            object memberValue = null;
            string append = null;

            //Create arguments
            PrintFormatArguments args = PrintFormatArguments.Create
                (workingType, value, path, member, index);

            builder.Append(format(args));

            if (workingType.Is<IDictionary>())
            {
                writeEnd = true;
                foreach (object dictionaryKey in (value as IDictionary).Keys)
                {
                    object dictionaryValue = (value as IDictionary)[dictionaryKey];
                    childPrefix = string.Concat(path, "[", dictionaryKey.WrapString(), "]");
                    builder.Append(Print(dictionaryValue, dictionaryKey.GetType(), childPrefix, member, dictionaryKey, format));
                    workingIndex++;
                }
            }
            else if (workingType.Is<IEnumerable>() && !workingType.Is<string>())
            {
                writeEnd = true;
                foreach (var enumerableValue in (value as IEnumerable))
                {
                    childPrefix = string.Concat(path, "[", workingIndex, "]");
                    builder.Append(Print(enumerableValue, enumerableValue.GetType(), childPrefix, member, workingIndex, format));
                    workingIndex++;
                }
            }
            else if (!workingType.IsSystemType())
            {
                writeEnd = true;

                //Loop through all properties of the object
                foreach (MemberInfo subMember in workingType.GetMembers())
                {
                    append = null;
                    memberValue = null;

                    if (subMember.MemberType == MemberTypes.Property)
                    {
                        PropertyInfo property = subMember as PropertyInfo;
                        if (property.GetGetMethod().IsPublic && !property.GetGetMethod().IsStatic)
                        {
                            memberType = property.PropertyType;
                            memberValue = property.GetValue(value, null);
                        }
                    }
                    else if (subMember is FieldInfo)
                    {
                        FieldInfo field = subMember as FieldInfo;

                        if (field.IsPublic && !field.IsStatic)
                        {
                            memberType = field.FieldType;
                            memberValue = field.GetValue(value);
                        }
                    }
                    else if (subMember is MethodInfo)
                    {
                        MethodInfo method = subMember as MethodInfo;
                        if (method.IsPublic && method.GetParameters().Length == 0 &&
                            !method.IsSpecialName && !method.IsStatic)
                        {
                            memberType = method.ReturnType;
                            memberValue = method.Invoke(value, null);
                            append = "()";
                        }
                    }

                    if (memberValue!=null)
                    {
                        childPrefix = string.Concat(path,".",subMember.Name, append);
                        builder.Append(Print(memberValue, memberType, childPrefix, subMember, null, format));
                    }
                }
            }

            if (writeEnd)
            {
                //Create arguments
                args = PrintFormatArguments.Create(workingType, value, 
                    path, member, index, WriteTypeEnum.WriteEnd);

                builder.Append(format(args));
            }

            return builder.ToString();
        }

        /// <summary>
        /// Standard default implementation for printing objects
        /// </summary>
        private static string DefaultPrintObjectValue(PrintFormatArguments args)
        {
            //Only print content if its a system type
            if (args.Type.IsSystemType())
            {
                if (!args.Type.Is<string>())
                    if (args.Type.Is<IEnumerable>())
                        return null;

                string displayValue = args.Value.WrapString();

                return string.Concat(args.Path, "=", displayValue, "\n");
            }
            return null;
        }

        /// <summary>
        /// Wraps an object with string quotes (if string)
        /// </summary>
        public static string WrapString(this object value)
        {
            if (value == null)
                return null;

            return (value is string) ? string.Concat("\"", value.ToString(), "\"") : value.ToString();
        }


    }

    /// <summary>
    /// Arguments for print format implementations
    /// </summary>
    public struct PrintFormatArguments
    {
        /// <summary>
        /// The type of object being written
        /// </summary>
        public Type Type { get; set; }

        /// <summary>
        /// The value being written
        /// </summary>
        public object Value { get; set; }

        /// <summary>
        /// The path of this value within its parent object structure
        /// </summary>
        public string Path { get; set; }

        /// <summary>
        /// Whether this value is a object member
        /// </summary>
        public MemberInfo MemberInfo { get; set; }

        /// <summary>
        /// Whether this value is an index of an enumerable type
        /// </summary>
        public object Index { get; set; }

        /// <summary>
        /// The format type
        /// </summary>
        public WriteTypeEnum FormatType { get; set; }

        /// <summary>
        /// Create a argument structure
        /// </summary>
        public static PrintFormatArguments Create(
            Type type,
            object value,
            string path,
            MemberInfo memberInfo,
            object index,
            WriteTypeEnum formatType = WriteTypeEnum.WriteStart
            )
        {
            //Create arguments
            PrintFormatArguments args = new PrintFormatArguments();
            args.Type = type;
            args.Value = value;
            args.Path = path;
            args.MemberInfo = memberInfo;
            args.Index = index;
            args.FormatType = formatType;
            return args;
        }
    }
}
